﻿using Abp.Timing;
using DotNetCore.CAP;
using Hangfire;
using Hicap.QRCodeWarehouse.Processor.Const;
using Hicap.QRCodeWarehouse.Processor.Helper;
using Hicap.QRCodeWarehouse.Processor.Manager;
using Hicap.QRCodeWarehouse.Processor.Model;
using MongoDB.Bson;
using MongoDB.Driver;
using Serilog;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;

namespace Hicap.QRCodeWarehouse.Processor.Service
{
    /// <summary>
    /// Generate qrcodes services
    /// </summary>
    public class CodeGenService : ServiceBase, ICodeGenService, ICapSubscribe
    {
        private readonly ICodeGenManager _codeGenManager;
        private readonly IMonitorManager _monitorManager;
        private readonly ICapPublisher _capBus;
        private static readonly object LockObject = new object();
        private readonly IMongoClient _mongoClient;

        public CodeGenService(IMonitorManager monitorManager, ICapPublisher capBus, ICodeGenManager codeGenManager, IMongoClient mongoClient)
        {
            _monitorManager = monitorManager;
            _capBus = capBus;
            _codeGenManager = codeGenManager;
            _mongoClient = mongoClient;
        }

        /// <summary>
        /// Reponse import action
        /// </summary>
        /// <param name="input">ImportCommandDto</param>
        /// <returns></returns>
        [CapSubscribe(MessageQueueChanelConst.CodeGenServiceImportAction, Group = SysConst.SysName)]
        public async Task<ActionResult> DispatchCodeImportAction(ImportCommandDto input)
        {
            Stopwatch watch = new Stopwatch();
            try
            {
                watch.Start();
                //get min taskId;
                int minTaskId;
                lock (LockObject)
                {
                    minTaskId = -1 * input.FileRepositoryId;
                }

                if (input.GenerationRuleType != 2)
                {
                    await UpsertQrcodes(input, minTaskId);
                }

                if (input.GenerationRuleType == 2)
                {
                    List<QrCodeLinkedRecord> datas = await UpsertQrLinkedCodes(input, minTaskId);

                    //backgroud update record info
                    BackgroundJob.Schedule<ICodeGenManager>((x) => x.UpdateCodeLinked(datas, input), TimeSpan.FromSeconds(1));
                }

                return new ActionResult(MessageQueueChanelConst.CodeGenServiceImportAction, true, watch.ElapsedMilliseconds, Clock.Now, input);
            }
            catch (Exception e)
            {
                return new ActionResult(MessageQueueChanelConst.CodeGenServiceImportAction, false, watch.ElapsedMilliseconds,
                    Clock.Now, input, e);
            }
        }

        private async Task<List<QrCodeLinkedRecord>> UpsertQrLinkedCodes(ImportCommandDto input, int minTaskId)
        {
            using (IClientSessionHandle clientSession = await _mongoClient.StartSessionAsync())
            {
                try
                {
                    clientSession.StartTransaction();

                    //exract zip data from zip file by config
                    List<QrCodeLinkedRecord> datas =
                        await _codeGenManager.ExractDataFromZip<QrCodeLinkedRecord>(input,
                            qrCodeRecord =>
                            {
                                qrCodeRecord.CreateTime = Clock.Now;
                                qrCodeRecord.TaskId = minTaskId;
                                qrCodeRecord.PrimaryCodeType = 0;
                                if (!input.IsInnerTypeBePrimary)
                                {
                                    string temp = qrCodeRecord.Code;
                                    qrCodeRecord.Code = qrCodeRecord.LinkedCode;
                                    qrCodeRecord.LinkedCode = temp;
                                    qrCodeRecord.PrimaryCodeType = 1;
                                }
                            });

                    //import datas to db
                    await ImportRecordDatas(input, minTaskId, datas, GetQrcodeLinkedUpdateDefinition,
                        clientSession);

                    await clientSession.CommitTransactionAsync();

                    return datas;
                }
                catch
                {
                    await clientSession.AbortTransactionAsync();
                    throw;
                }
            }
        }

        private async Task UpsertQrcodes(ImportCommandDto input, int minTaskId)
        {
            using (IClientSessionHandle clientSession = await _mongoClient.StartSessionAsync())
            {
                try
                {
                    clientSession.StartTransaction();
                    //exract zip data from zip file by config
                    List<QrCodeRecord> datas = await _codeGenManager.ExractDataFromZip<QrCodeRecord>(input,
                        qrCodeRecord =>
                        {
                            qrCodeRecord.CreateTime = Clock.Now;
                            qrCodeRecord.IsDownloaded = false;
                            qrCodeRecord.IsUsed = false;
                            qrCodeRecord.TaskId = minTaskId;
                        });

                    //import datas to db
                    await ImportRecordDatas(input, minTaskId, datas, GetQrcodeUpdateDefinition, clientSession);

                    await clientSession.CommitTransactionAsync();
                }
                catch
                {
                    await clientSession.AbortTransactionAsync();
                    throw;
                }
            }
        }

        /// <summary>
        /// Import datas to db
        /// </summary>
        /// <param name="input">ImportCommandDto</param>
        /// <param name="minTaskId">int</param>
        /// <param name="datas">List QrCodeRecord</param>
        /// <param name="definitionFunc"></param>
        /// <param name="clientSession"></param>
        /// <returns></returns>
        private async Task ImportRecordDatas<T>(ImportCommandDto input, int minTaskId, List<T> datas, Func<T, Tuple<FilterDefinition<T>, UpdateDefinition<T>>> definitionFunc, IClientSessionHandle clientSession)
        {
            string handle = Guid.NewGuid().ToString("N");
            ImportDataDto<T> importConfig = new ImportDataDto<T>()
            {
                TotalCount = datas.Count,
                TaskId = minTaskId,
                GenerationRuleType = input.GenerationRuleType,
                Handle = handle,
                ProductCode = input.ProductCode,
                FileName = input.FileName,
                ProductId = input.ProductId,
                IsAutoIncrement = false,
                SubordinateCompanyId = input.SubordinateCompanyId,
                UserId = input.UserId
            };

            //if datas count <= Count Interval, one time import.
            if (datas.Count <= SysConst.CountInterval)
            {
                importConfig.CurrentLoopTime = 0;
                importConfig.Datas = datas;
                importConfig.Amount = datas.Count;

                await ImportRecordDatas(importConfig, definitionFunc, clientSession);
            }
            else
            {
                //loop import
                for (int i = 0; i < Math.Ceiling((decimal)datas.Count / SysConst.CountInterval); i++)
                {
                    List<T> subDatas = datas.Skip(i * SysConst.CountInterval).Take(SysConst.CountInterval).ToList();

                    importConfig.CurrentLoopTime = i;
                    importConfig.Datas = subDatas;
                    importConfig.Amount = subDatas.Count;

                    await ImportRecordDatas(importConfig, definitionFunc, clientSession);
                }
            }
        }

        /// <summary>
        ///  Import datas to db
        /// </summary>
        /// <param name="input">ImportDataDto</param>
        /// <param name="definitionFunc"></param>
        /// <param name="clientSession"></param>
        /// <returns></returns>
        private async Task ImportRecordDatas<T>(ImportDataDto<T> input, Func<T, Tuple<FilterDefinition<T>, UpdateDefinition<T>>> definitionFunc, IClientSessionHandle clientSession)
        {
            //get collection
            IMongoCollection<T> recordCollection = _codeGenManager.GetCollection<T>($"{input.ProductCode}_{input.GenerationRuleType}");

            //upsert monitor info
            await UpsertWarhouseMonitor(input, clientSession);

            //insert datas
            if (await _codeGenManager.InsertRecordAsync<T>(input.Datas.ToArray(), recordCollection, definitionFunc, clientSession))
            {
                //publish completed message to ctrl client
                await _capBus.PublishAsync(MessageQueueChanelConst.CodeGenServiceImportFeedBack, new ImportFeedbackDto()
                {
                    Handle = input.Handle,
                    FileName = input.FileName,
                    TaskId = input.TaskId,
                    ProductId = input.ProductId,
                    ProductCode = input.ProductCode,
                    SubordinateCompanyId = input.SubordinateCompanyId,
                    Amount = input.Amount,
                    CurrentLoopTime = input.CurrentLoopTime,
                    TotalCount = input.TotalCount,
                    GenerationRuleType = input.GenerationRuleType,
                    //current progress
                    Progress = (decimal)(input.CurrentLoopTime * SysConst.CountInterval + input.Amount) / input.TotalCount,
                    UserId = input.UserId,
                }, MessageQueueChanelConst.CodeGenServiceImportFeedBack + ".End");

                Log.Information($"Task [{input.TaskId}:Loop [{input.CurrentLoopTime}] is completed !");
            }
        }

        /// <summary>
        /// response generate codes action
        /// </summary>
        /// <param name="input">TaskCommandDto</param>
        /// <returns></returns>
        [CapSubscribe(MessageQueueChanelConst.CodeGenServiceGenAction, Group = SysConst.SysName)]
        public async Task<ActionResult> DispatchCodeGenAction(TaskCommandDto input)
        {
            Stopwatch watch = new Stopwatch();
            watch.Start();
            try
            {
                Log.Information($"Recevied Data:{input.ToJson()}");

                //check input
                if (input.Amount <= 0) return new ActionResult(MessageQueueChanelConst.CodeGenServiceGenAction, false, watch.ElapsedMilliseconds, Clock.Now, input, new Exception("Amount less than 0"));
                if (input.AutoIncrementLength <= 0) return new ActionResult(MessageQueueChanelConst.CodeGenServiceGenAction, false, watch.ElapsedMilliseconds, Clock.Now, input, new Exception("AutoIncrementLength less than 0"));
                if (input.ProductId <= 0) return new ActionResult(MessageQueueChanelConst.CodeGenServiceGenAction, false, watch.ElapsedMilliseconds, Clock.Now, input, new Exception("ProductId less than 0"));
                if (input.RandomLength <= 0) return new ActionResult(MessageQueueChanelConst.CodeGenServiceGenAction, false, watch.ElapsedMilliseconds, Clock.Now, input, new Exception("RandomLength less than 0"));
                if (input.TaskId <= 0) return new ActionResult(MessageQueueChanelConst.CodeGenServiceGenAction, false, watch.ElapsedMilliseconds, Clock.Now, input, new Exception("TaskId is null"));
                //            if (input.SubordinateCompanyId <= 0) return return new ActionResult(MessageQueueChanelConst.CodeGenServiceGenAction,false, watch.ElapsedMilliseconds,Clock.Now);

                //get collection
                IMongoCollection<QrCodeRecord> recordCollection = _codeGenManager.GetCollection<QrCodeRecord>($"{input.ProductCode}_{input.GenerationRuleType}");

                using (IClientSessionHandle clientSession = await _mongoClient.StartSessionAsync())
                {
                    try
                    {
                        clientSession.StartTransaction();

                        //upsert monitor info,add count and add audit logs
                        WarhouseMonitor monitor = await UpsertWarhouseMonitor(input, clientSession);

                        //output info
                        TaskFeedbackDto output = new TaskFeedbackDto()
                        {
                            ProgressPct = 0,
                            ProductId = input.ProductId,
                            ProductCode = input.ProductCode,
                            GenerationRuleType = input.GenerationRuleType,
                            SubordinateCompanyId = input.SubordinateCompanyId,
                            TaskId = input.TaskId,
                            TotalAmount = input.Amount,
                            UserId = input.UserId
                        };

                        //insert data
                        await InsertData(recordCollection, input, monitor, clientSession, GetQrcodeUpdateDefinition,
                            async (progress) =>
                            {
                                //progress
                                output.ProgressPct = progress;

                                //publish current progress
                                await PublishMessageAsync(progress, output);
                            });

                        await clientSession.CommitTransactionAsync();

                        Log.Information($"Task [{output.TaskId}] is completed !");
                    }
                    catch
                    {
                        await clientSession.AbortTransactionAsync();
                    }
                }
                return new ActionResult(MessageQueueChanelConst.CodeGenServiceGenAction, true, watch.ElapsedMilliseconds, Clock.Now, input);
            }
            catch (Exception e)
            {
                return new ActionResult(MessageQueueChanelConst.CodeGenServiceGenAction, false, watch.ElapsedMilliseconds,
                    Clock.Now, input, e);
            }
        }

        private Tuple<FilterDefinition<QrCodeLinkedRecord>, UpdateDefinition<QrCodeLinkedRecord>> GetQrcodeLinkedUpdateDefinition
            (QrCodeLinkedRecord entity)
        {
            FilterDefinition<QrCodeLinkedRecord> filter = Builders<QrCodeLinkedRecord>.Filter.Eq(x => x.Code, entity.Code);

            UpdateDefinition<QrCodeLinkedRecord> update = Builders<QrCodeLinkedRecord>.Update
                .Set(x => x.CreateTime, Clock.Now);

            return new Tuple<FilterDefinition<QrCodeLinkedRecord>, UpdateDefinition<QrCodeLinkedRecord>>(filter, update);
        }

        private Tuple<FilterDefinition<QrCodeRecord>, UpdateDefinition<QrCodeRecord>> GetQrcodeUpdateDefinition
            (QrCodeRecord entity)
        {
            FilterDefinition<QrCodeRecord> filter = Builders<QrCodeRecord>.Filter.Eq(x => x.Code, entity.Code);

            UpdateDefinition<QrCodeRecord> update = Builders<QrCodeRecord>.Update
              .Set(x => x.IsUsed, true)
              .Set(x => x.IsDownloaded, false)
              .Set(x => x.DownloadedTime, null)
              .Set(x => x.UsedTime, Clock.Now);

            return new Tuple<FilterDefinition<QrCodeRecord>, UpdateDefinition<QrCodeRecord>>(filter, update);
        }

        /// <summary>
        /// Publish message
        /// </summary>
        /// <param name="progress">decimal</param>
        /// <param name="output">TaskFeedbackDto</param>
        /// <returns></returns>
        private async Task PublishMessageAsync(decimal progress, TaskFeedbackDto output)
        {
            //publish output to ctrl client
            await _capBus.PublishAsync(MessageQueueChanelConst.CodeGenServiceFeedBack, output, MessageQueueChanelConst.CodeGenServiceFeedBack + ".End");

            Log.Information($"Task [{output.TaskId}] Insert  data progress percent :{progress:p3}");
        }

        [CapSubscribe(MessageQueueChanelConst.CodeGenServiceImportFeedBack + ".End", Group = SysConst.SysName)]
        public void FinalImportFeedBackMessage(ActionResult result)
        {
            if (result.IsSuccessed)
                Log.Information($"FeedBack Finished:{MessageQueueChanelConst.CodeGenServiceImportFeedBack}--{result}");
            else
                Log.Error($"FeedBack have a Error:{MessageQueueChanelConst.CodeGenServiceImportFeedBack}--{result}");
        }

        [CapSubscribe(MessageQueueChanelConst.CodeGenServiceFeedBack + ".End", Group = SysConst.SysName)]
        public void FinalFeedBackMessage(ActionResult result)
        {
            if (result.IsSuccessed)
                Log.Information($"FeedBack Finished:{MessageQueueChanelConst.CodeGenServiceFeedBack}--{result}");
            else
                Log.Error($"FeedBack have a Error:{MessageQueueChanelConst.CodeGenServiceFeedBack}--{result}");
        }

        /// <summary>
        /// generate codes(kernel)
        /// </summary>
        /// <param name="recordCollection">IMongoCollection&lt;QrCodeRecord&gt;</param>
        /// <param name="input">TaskCommandDto</param>
        /// <param name="monitor">WarhouseMonitor</param>
        /// <param name="definitionFunc"></param>
        /// <param name="progress">Action&lt;decimal&gt;</param>
        /// <param name="clientSession">IClientSessionHandle</param>
        /// <returns></returns>
        private async Task InsertData(IMongoCollection<QrCodeRecord> recordCollection, TaskCommandDto input, WarhouseMonitor monitor, IClientSessionHandle clientSession, Func<QrCodeRecord, Tuple<FilterDefinition<QrCodeRecord>, UpdateDefinition<QrCodeRecord>>> definitionFunc, Action<decimal> progress = null)
        {
            //generate code config
            CodeGenConfig config = new CodeGenConfig()
            {
                //length
                IncreamentLength = input.AutoIncrementLength,
                Length = input.RandomLength,
                PrefixUrl = input.PrefixUrl,
                //total count
                Num = input.Amount <= SysConst.CountInterval ? input.Amount : SysConst.CountInterval,
                //start index
                StartIncreament = input.GenerationRuleType == 0 ?
                    (monitor.InnerSequence + 1) : (monitor.OutterSequence + 1),

                //have contains character string
                IsOnlyNumberCode = input.IsOnlyNumberCode
            };

            //loop generation,insert datas for every SysConst.CountInterval records
            int loopSucceedCount = 0;
            while (loopSucceedCount * SysConst.CountInterval < input.Amount)
            {
                //pending count
                int todoCount = input.Amount - loopSucceedCount * SysConst.CountInterval;

                //one time
                if (todoCount < SysConst.CountInterval) config.Num = todoCount;

                //get batch qrcodes(increament and random string)
                QrCodeRecord[] list = CodeHelper.GetIncreamentAndRandomCode(config, input.TaskId, input.PrefixUrl);

                //insert datas
                if (await _codeGenManager.InsertRecordAsync(list, recordCollection, definitionFunc, clientSession))
                {
                    await Task.Run(() =>
                    {
                        lock (LockObject)
                        {
                            //progress refresh
                            decimal progressPct = (Convert.ToDecimal(input.Amount - todoCount)) / input.Amount;

                            //start increament index refresh
                            config.StartIncreament += SysConst.CountInterval;

                            //in pending
                            if (progressPct != 1)
                            {
                                //invoke publish progress message to client
                                progress?.Invoke(progressPct);
                            }

                            //continue loop
                            loopSucceedCount++;
                        }
                    });
                }
            }

            //complete and publish message
            progress?.Invoke(1);
        }

        /// <summary>
        /// refresh warhouse monitor info
        /// </summary>
        /// <param name="input">IWarhouseMonitor</param>
        /// <param name="clientSession"></param>
        /// <returns>WarhouseMonitor</returns>
        private async Task<WarhouseMonitor> UpsertWarhouseMonitor(IWarhouseMonitor input, IClientSessionHandle clientSession)
        {
            //get exsit monitor info
            WarhouseMonitor data = await _monitorManager.GetWarhouseMonitorAsync(input.ProductId);

            //add audit log
            AuditLog auditLog = new AuditLog() { TaskId = input.TaskId, TriggeringTime = Clock.Now };

            //monitor info is not exist,create new monitor.
            if (data == null) data = new WarhouseMonitor()
            {
                ProductId = input.ProductId
            };
            data.ProductCode = input.ProductCode;
            data.SubordinateCompanyId = input.SubordinateCompanyId;

            //add count
            if (input.GenerationRuleType == 0)
            {
                if (input.IsAutoIncrement)
                    data.InnerSequence += input.Amount;
                data.TotalInnerCount += input.Amount;
            }
            else
            {
                if (input.IsAutoIncrement)
                    data.OutterSequence += input.Amount;
                data.TotalOutterCount += input.Amount;
            }

            //add audit log
            data.AuditLog.Add(auditLog);

            //upsert
            await _monitorManager.UpsertAsync(data, clientSession);

            return data;
        }
    }
}